unit imageselection;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  ButtonPanel, fpjson, fgl, LCLTranslator, ComCtrls, Spin, BGRABitmap,
  BGRABitmapTypes, imghelper, tilesetgraphicsunit;

type

  TImageSelectionType = (istImage, istFace, istSimpleCharset, istFullCharset);

  { TImageSelectionForm }

  TImageSelectionForm = class(TForm)
    DecisionButtonPanel: TButtonPanel;
    FilesListBox: TListBox;
    FilesSplitter: TSplitter;
    HueGroupBox: TGroupBox;
    PreviewPaintBox: TPaintBox;
    PreviewScrollBox: TScrollBox;
    SelectedPanel: TPanel;
    HueTrackBar: TTrackBar;
    HueSpinEdit: TSpinEdit;
    procedure CancelButtonClick(Sender: TObject);
    procedure FilesListBoxSelectionChange(Sender: TObject; User: boolean);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure HueSpinEditChange(Sender: TObject);
    procedure HueTrackBarChange(Sender: TObject);
    procedure OKButtonClick(Sender: TObject);
    procedure PreviewPaintBoxDblClick(Sender: TObject);
    procedure PreviewPaintBoxMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure PreviewPaintBoxPaint(Sender: TObject);
    procedure PreviewScrollBoxResize(Sender: TObject);
  private
    SelectionType: TImageSelectionType;
    Path: String; (* full path, with folder name *)
    HasNone: Boolean;
    HasTilesetItem: Boolean;
    UpdatingHueTrackBar: Boolean;

    ResultWasAccepted: Boolean;
    CurrentImage, RecolouredImage: TBGRABitmap;
    ResizedImage: TBGRACustomBitmap;
    TilesetGraphics: TTilesetGraphics;
    procedure FillFileList;
    procedure UpdatePreview;
    procedure UpdateResizedPreview;
    procedure FillSelectionResults;
    function IsNoneSelected: Boolean;
    function IsTilesetSelected: Boolean;
    function GetCellData(var AWidth, AHeight, PerLine: Integer): Boolean;
    function CountTilesetItems: Integer;
    function GetSelectedTilesetPart: Integer;
  public
    Index, Pattern, Direction, TileId, SelectedTilesetPart: Integer;
    TilesetItemSelected: Boolean;
    SelectedHue: Integer;
    SelectedFilename: String;
    function ShowFaceSelection(AFace: String; AIndex: Integer): Boolean;
    function ShowSimpleCharSelection(AChar: String; AIndex: Integer): Boolean;
    function ShowFullCharSelection(AChar: String; AIndex, APattern, ADir, ATileId: Integer; ATilesetGraphics: TTilesetGraphics): Boolean;
    function ShowSvBattlerSelection(ABattler: String): Boolean;
    function ShowEnemySelection(AEnemy: String; Hue: Integer; IsSv: Boolean): Boolean;
    function ShowTilesetImageSelection(OldFile: String): Boolean;
  end;

var
  ImageSelectionForm: TImageSelectionForm;

implementation

uses Math, globals, gameproject, filehelper;

{$R *.lfm}

procedure TImageSelectionForm.CancelButtonClick(Sender: TObject);
begin
  ResultWasAccepted := False;
  Close;
end;

procedure TImageSelectionForm.FilesListBoxSelectionChange(Sender: TObject;
  User: boolean);
begin
  UpdatePreview;
  Refresh;
end;

procedure TImageSelectionForm.FormClose(Sender: TObject;
  var CloseAction: TCloseAction);
begin
  FillSelectionResults;

  FilesListBox.Items.Clear;
  if ResizedImage <> nil then
    ResizedImage.Free;
  ResizedImage := nil;
  if CurrentImage <> nil then
    CurrentImage.Free;
  CurrentImage := nil;
end;

procedure TImageSelectionForm.HueSpinEditChange(Sender: TObject);
begin

  SelectedHue := HueSpinEdit.Value;

  UpdatingHueTrackBar := True;
  HueTrackBar.Position := SelectedHue;
  UpdatingHueTrackBar := False;

  if CurrentImage = nil then
    Exit;

  if RecolouredImage <> nil then begin
    RecolouredImage.Free;
    RecolouredImage := nil;
  end;

  if (HueSpinEdit.Value <> 0) and (HueSpinEdit.Value <> 360) then
    RecolouredImage := ChangeHue(CurrentImage, HueSpinEdit.Value);

  UpdateResizedPreview;
  Refresh
end;

procedure TImageSelectionForm.HueTrackBarChange(Sender: TObject);
begin
  if not UpdatingHueTrackBar then
    HueSpinEdit.Value := HueTrackBar.Position;
end;

procedure TImageSelectionForm.FillSelectionResults;
begin
  if IsNoneSelected then begin
    TilesetItemSelected := False;
    SelectedFilename := '';
    Index := 0;
  end
  else if IsTilesetSelected then begin
    TilesetItemSelected := True;
    SelectedFilename := '';
  end
  else begin
    TilesetItemSelected := False;
    SelectedFilename := FilesListBox.Items[FilesListBox.ItemIndex];
    if TGameProject.IsDollarName(SelectedFilename) then
      Index := 0;
  end;
end;

function TImageSelectionForm.IsNoneSelected: Boolean;
begin
  if HasNone then
    IsNoneSelected := FilesListBox.ItemIndex = 0
  else
    IsNoneSelected := False;
end;

function TImageSelectionForm.IsTilesetSelected: Boolean;
begin
  if HasTilesetItem then begin
    IsTilesetSelected := FilesListBox.ItemIndex >= FilesListBox.Count - CountTilesetItems; //TODO: preselect the correct tileset
  end
  else
    IsTilesetSelected := False;
end;

procedure TImageSelectionForm.OKButtonClick(Sender: TObject);
begin
  ResultWasAccepted := True;
  Close;
end;

procedure TImageSelectionForm.PreviewPaintBoxDblClick(Sender: TObject);
begin
  (* This causes problems on Windows: if you close the window by double-click,
  subsequent calls to show ImageSelectionForm will not show anything.
  I don't know what causes this, so let's keep it commented out for a while. *)
  {$IF not Defined(LCLWin32)}
  //TODO: check that this doesn't happen in other widgetsets
  ResultWasAccepted := True;
  Close;
  {$ENDIF}
end;

procedure TImageSelectionForm.PreviewPaintBoxMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  XIndex, YIndex: Integer;
  CellWidth: Integer = 0;
  CellHeight: Integer = 0;
  CellsPerLine: Integer = 0;
  ResizedCellWidth, ResizedCellHeight: Integer;
begin
  if GetCellData(CellWidth, CellHeight, CellsPerLine) then begin
    //TODO: move this our into a separate routine?
    ResizedCellWidth := CellWidth * ResizedImage.Width div CurrentImage.Width;
    ResizedCellHeight:=CellHeight*ResizedImage.Height div CurrentImage.Height;

    XIndex := X div ResizedCellWidth;
    YIndex := Y div ResizedCellHeight;

    Index := YIndex*CellsPerLine + XIndex;
    Refresh;
  end;
end;

procedure TImageSelectionForm.PreviewPaintBoxPaint(Sender: TObject);
var
  Dst, SelectionRect: TRect;
  CellWidth: Integer = 0;
  CellHeight: Integer = 0;
  CellsPerLine: Integer = 0;
  ResizedCellWidth, ResizedCellHeight: Double;
begin
  if (ResizedImage <> nil) and (CurrentImage <> nil) then begin
    Dst :=Rect(0, 0, PreviewPaintBox.ClientWidth, PreviewPaintBox.ClientHeight);
    //PreviewPaintBox.Canvas.StretchDraw(Dst, ResizedImage);
    ResizedImage.Draw(PreviewPaintBox.Canvas, Dst, False);

    if GetCellData(CellWidth, CellHeight, CellsPerLine) then begin
      //TODO: draw index
      ResizedCellWidth := CellWidth * ResizedImage.Width / CurrentImage.Width;
      ResizedCellHeight:=CellHeight*ResizedImage.Height / CurrentImage.Height;

      SelectionRect.Left := Floor((Index mod CellsPerLine) * ResizedCellWidth);
      SelectionRect.Top := Floor((Index div CellsPerLine) * ResizedCellHeight);
      SelectionRect.Right := Ceil(SelectionRect.Left + ResizedCellWidth);
      SelectionRect.Bottom := Ceil(SelectionRect.Top + ResizedCellHeight);

      PreviewPaintBox.Canvas.Brush.Style := bsClear;
      PreviewPaintBox.Canvas.Pen.Color := clWhite;
      PreviewPaintBox.Canvas.Pen.Width := 2;
      PreviewPaintBox.Canvas.Rectangle(SelectionRect);

      PreviewPaintBox.Canvas.Pen.Color := clBlack;
      SelectionRect.Inflate(-2, -2, -2, -2);
      PreviewPaintBox.Canvas.Rectangle(SelectionRect);

      PreviewPaintBox.Canvas.Pen.Color := clWhite;
      SelectionRect.Inflate(-2, -2, -2, -2);
      PreviewPaintBox.Canvas.Rectangle(SelectionRect);

      //ResizedCellHeight

    end;
  end;
end;

function TImageSelectionForm.GetCellData(var AWidth, AHeight, PerLine: Integer): Boolean;
begin
  if IsTilesetSelected then begin
    AWidth := 48; //TODO: adapt for other tile sizes
    AHeight := 48; //TODO: adapt for other tile sizes
    PerLine := 8;
    GetCellData := True;
  end
  else if IsNoneSelected then begin
    GetCellData := False;
  end
  else if CurrentImage <> nil then begin
    if SelectionType = istFace then begin
      AWidth := 144;
      AHeight := 144;
      PerLine := 4;
      GetCellData := True;
    end
    else if SelectionType = istSimpleCharset then begin
      if TGameProject.IsDollarName(FilesListBox.Items[FilesListBox.ItemIndex]) then begin
        AWidth := CurrentImage.Width;
        AHeight := CurrentImage.Height;
        PerLine := 1;
        GetCellData := True;
      end
      else begin
        AWidth := CurrentImage.Width div 4;
        AHeight := CurrentImage.Height div 2;
        PerLine := 4;
        GetCellData := True;
      end;
    end
    else if SelectionType = istFullCharset then begin
      if TGameProject.IsDollarName(FilesListBox.Items[FilesListBox.ItemIndex]) then begin
        AWidth := CurrentImage.Width div 3;
        AHeight := CurrentImage.Height div 4;
        PerLine := 3;
        GetCellData := True;
      end
      else begin
        AWidth := CurrentImage.Width div (3*4);
        AHeight := CurrentImage.Height div (4*2);
        PerLine := 3*4;
        GetCellData := True;
      end;
    end
    else begin
      GetCellData := False;
    end;
  end;
end;

function TImageSelectionForm.CountTilesetItems: Integer;
var
  I: Integer;
begin
  CountTilesetItems := 0;
  if HasTilesetItem and (TilesetGraphics <> nil) then
    for I := 5 to High(TilesetGraphics.Bitmaps) do
        if TilesetGraphics.HasSet(I) then
          Inc(CountTilesetItems);
end;

function TImageSelectionForm.GetSelectedTilesetPart: Integer;
var
  I, StartIndex, StartListItem: Integer;
begin
  GetSelectedTilesetPart := 0;

  if not HasTilesetItem or (TilesetGraphics = nil) then begin
    Exit;
  end;

  StartIndex := 8;
  StartListItem := FilesListBox.Count -1;

  repeat
    if TilesetGraphics.HasSet(StartIndex) then
      if StartListItem = FilesListBox.ItemIndex then begin
        GetSelectedTilesetPart := StartIndex;
        Exit
      end else
        Dec(StartListItem);

    Dec(StartIndex);
  until StartIndex <= 4;
end;

procedure TImageSelectionForm.PreviewScrollBoxResize(Sender: TObject);
begin
  UpdateResizedPreview;
end;

procedure TImageSelectionForm.FillFileList;
var
  PreselectedIndex: Integer = -1;
  I: Integer;
  FileNames: TFilenameList;

  function GetAvailableTilesetId: Integer;
  var
    CurrentNumber, SetIndex: Integer;
  begin
    Result := 0;

    CurrentNumber := 0;

    for SetIndex := 5 to 8 do
      if TilesetGraphics <> nil then
        if TilesetGraphics.HasSet(SetIndex) then
          if SelectedTilesetPart = SetIndex - 5 then begin
            Result := CurrentNumber;
            Exit;
          end else
            Inc(CurrentNumber);
  end;

  procedure AddTilesetItems;
  begin
    if (TilesetGraphics <> nil) then begin
      if TilesetGraphics.HasSet(5) then
        FilesListBox.Items.Add(TilesBFilename);
      if TilesetGraphics.HasSet(6) then
        FilesListBox.Items.Add(TilesCFilename);
      if TilesetGraphics.HasSet(7) then
        FilesListBox.Items.Add(TilesDFilename);
      if TilesetGraphics.HasSet(8) then
        FilesListBox.Items.Add(TilesEFilename);
    end;

    if TilesetItemSelected then begin
      PreselectedIndex := FilesListBox.Items.Count
                           - CountTilesetItems + GetAvailableTilesetId;
    end;
  end;

begin
  FilesListBox.Items.BeginUpdate;
  FilesListBox.Items.Clear;
  if HasNone then begin
    FilesListBox.Items.Add(NoneFilename);
    if (SelectedFilename = '') and not TilesetItemSelected then begin
      PreselectedIndex := FilesListBox.Items.Count - 1;
    end;
  end;

  //TODO: show files in subfolders???
  //TODO: make sure FilesListBox doesn't mess with our filenames (what about names w/ line feed in them? do we even need this brain damage?)

  FileNames := FillFilenameList(Path);

  for I := 0 to FileNames.Count - 1 do begin
    FilesListBox.Items.Add(FileNames[I]);
    if SelectedFilename = FileNames[I] then begin
      PreselectedIndex := FilesListBox.Items.Count - 1;
    end;
  end;

  FileNames.Free;

  AddTilesetItems;

  FilesListBox.Items.EndUpdate;

  if PreselectedIndex > -1 then
    FilesListBox.ItemIndex := PreselectedIndex;
end;

procedure TImageSelectionForm.UpdatePreview;
var
  Filename: String = '';
  NewImage: TBGRABitmap = nil;
  BitmapToCopy: TBGRABitmap = nil;
begin
  if CurrentImage <> nil then begin
    CurrentImage.Free;
    CurrentImage := nil;
  end;
  if RecolouredImage <> nil then begin
    RecolouredImage.Free;
    RecolouredImage := nil;
  end;

  if IsNoneSelected then begin
    //TODO: (None) item handling
  end
  else if IsTilesetSelected then begin
    BitmapToCopy := TilesetGraphics.Bitmaps[GetSelectedTilesetPart];
    NewImage := TBGRABitmap.Create(BitmapToCopy.Width div 2, BitmapToCopy.Height * 2, BGRAWhite);
    NewImage.CanvasBGRA.CopyRect(0, 0, BitmapToCopy, Rect(
                                    0,
                                    0,
                                    BitmapToCopy.Width div 2,
                                    BitmapToCopy.Height));
    NewImage.CanvasBGRA.CopyRect(0, BitmapToCopy.Height, BitmapToCopy,
                                    Rect(
                                         BitmapToCopy.Width div 2,
                                         0,
                                         BitmapToCopy.Width,
                                         BitmapToCopy.Height));

    SelectedTilesetPart := GetSelectedTilesetPart - 5;

    CurrentImage := NewImage;
    //TODO: (Tiles) item handling
  end
  else begin
      try
        if (FilesListBox.ItemIndex >= 0) and (FilesListBox.ItemIndex < FilesListBox.Count) then
          Filename := Path + DirectorySeparator
                           + FilesListBox.Items[FilesListBox.ItemIndex]
                           + '.png';
        if FileExists(Filename) then
          NewImage := TBGRABitmap.Create(Filename);
      except
        if NewImage <> nil then
          NewImage.Free;
      end;

      CurrentImage := NewImage;
      if (CurrentImage <> nil) and (HueSpinEdit.Value <> 0) and (HueSpinEdit.Value <> 360) then
        RecolouredImage := ChangeHue(CurrentImage, HueSpinEdit.Value);
  end;

  UpdateResizedPreview;
end;

procedure TImageSelectionForm.UpdateResizedPreview;
var
  ResizedWidth, ResizedHeight: Integer;
  ImgToResize: TBGRABitmap;
begin
  if (CurrentImage <> nil) and (PreviewScrollBox.ClientWidth > 20) then begin
    if ResizedImage <> nil then begin
      ResizedImage.Free;
      ResizedImage := nil;
    end;

    if RecolouredImage <> nil then
      ImgToResize := RecolouredImage
    else
      ImgToResize := CurrentImage;

    (* Using PreviewScrollBox.ClientWidth causes an infinite loop here, so
     let's do this hack and hope scrollbar is around 20px. (If it's not,
     a small edge of the image will be hidden, which is acceptable.) *)
    ResizedWidth := PreviewScrollBox.Width - 20;
    ResizedHeight := ImgToResize.Height
                               * ResizedWidth
                               div Max(ImgToResize.Width, 1);

    PreviewPaintBox.ClientWidth := ResizedWidth;
    PreviewPaintBox.ClientHeight := ResizedHeight;
    (* Left can be changed when flipping for RTL, so ensure it's correct *)
    PreviewPaintBox.Left := 0;

    ResizedImage := ImgToResize.Resample(ResizedWidth, ResizedHeight, rmSimpleStretch);
  end;
end;

function TImageSelectionForm.ShowFaceSelection(AFace: String; AIndex: Integer): Boolean;
begin
  ResultWasAccepted := False;
  SelectionType := istFace;
  SelectedFilename := AFace;
  HasTilesetItem := False;
  TilesetItemSelected := False;
  Index := AIndex;
  Path := Game.GetImageSubfolderPath('faces');
  HueGroupBox.Hide;
  HueSpinEdit.Value := 0;
  HasNone := True;

  FillFileList;
  UpdatePreview;

  ShowModal;

  ShowFaceSelection := ResultWasAccepted;
end;

function TImageSelectionForm.ShowSimpleCharSelection(AChar: String; AIndex: Integer): Boolean;
begin
  ResultWasAccepted := False;
  SelectionType := istSimpleCharset;
  SelectedFilename := AChar;
  TilesetItemSelected := False; 
  HasTilesetItem := False;
  Index := AIndex;
  Path := Game.GetImageSubfolderPath('characters');
  HueGroupBox.Hide;
  HueSpinEdit.Value := 0;
  HasNone := True;

  FillFileList;
  UpdatePreview;

  ShowModal;

  ShowSimpleCharSelection := ResultWasAccepted;
end;

function TImageSelectionForm.ShowFullCharSelection(AChar: String; AIndex,
  APattern, ADir, ATileId: Integer; ATilesetGraphics: TTilesetGraphics): Boolean;
var
  CharsPerLine: Integer = 4;
  CharOffset: Integer = 0;
begin
  ResultWasAccepted := False;
  SelectionType := istFullCharset;
  SelectedFilename := AChar;
  TilesetItemSelected := False;

  if TGameProject.IsDollarName(AChar) then
    CharsPerLine := 1;

  if AIndex > 3 then
    CharOffset := 48 + ((AIndex - 4) * 3)
  else
    CharOffset := (AIndex * 3);

  Index := CharOffset + ((ADir div 2 - 1) * (CharsPerLine * 3)) + APattern;
  Path := Game.GetImageSubfolderPath('characters');
  HueGroupBox.Hide;
  HueSpinEdit.Value := 0;
  HasNone := True;
  HasTilesetItem := True;
  TilesetGraphics := ATilesetGraphics;
  TileId := ATileId;
  SelectedTilesetPart := TileId div 256;
  TilesetItemSelected := SelectedFilename = '';
  if TilesetItemSelected then
    Index := TileId mod 256;

  FillFileList;
  UpdatePreview;

  ShowModal;

  if TilesetItemSelected then begin
    SelectedFilename := '';
    TileId := Index + (SelectedTilesetPart * 256); //TODO: preset SelectedTilesetPart
  end else begin
    if TGameProject.IsDollarName(SelectedFilename) then
      CharsPerLine := 1
    else
      CharsPerLine := 4;

    Direction := (((Index div (CharsPerLine * 3)) mod 4) + 1) * 2;
    Pattern := Index mod 3;

    if Index < 48 then
      Index := (Index mod (CharsPerLine * 3)) div 3
    else
      Index := 4 + (Index mod (CharsPerLine * 3)) div 3
  end;

  ShowFullCharSelection := ResultWasAccepted;
end;

function TImageSelectionForm.ShowSvBattlerSelection(ABattler: String): Boolean;
begin
  ResultWasAccepted := False;
  SelectionType := istImage;
  SelectedFilename := ABattler;
  TilesetItemSelected := False;
  HasTilesetItem := False;
  Path := Game.GetImageSubfolderPath('sv_actors');
  HueGroupBox.Hide;
  HueSpinEdit.Value := 0;
  HasNone := True;

  FillFileList;
  UpdatePreview;

  ShowModal;

  ShowSvBattlerSelection := ResultWasAccepted;
end;

function TImageSelectionForm.ShowEnemySelection(AEnemy: String; Hue: Integer;
  IsSv: Boolean): Boolean;
begin
  ResultWasAccepted := False;
  SelectionType := istImage;
  SelectedFilename := AEnemy;
  TilesetItemSelected := False; 
  HasTilesetItem := False;
  if IsSv then
    Path := Game.GetImageSubfolderPath('sv_enemies')
  else
    Path := Game.GetImageSubfolderPath('enemies');
  HueGroupBox.Show;
  HueSpinEdit.Value := Hue;
  HueTrackBar.Position := Hue;
  HasNone := True;

  FillFileList;
  UpdatePreview;

  ShowModal;

  ShowEnemySelection := ResultWasAccepted;
end;

function TImageSelectionForm.ShowTilesetImageSelection(OldFile: String
  ): Boolean;
begin
  ResultWasAccepted := False;
  SelectionType := istImage;
  SelectedFilename := OldFile;
  TilesetItemSelected := False;  
  HasTilesetItem := False;
  Path := Game.GetImageSubfolderPath('tilesets');
  HueGroupBox.Hide;
  HasNone := True;

  FillFileList;
  UpdatePreview;

  ShowModal;

  ShowTilesetImageSelection := ResultWasAccepted;
end;

end.

